package indigo.core.config;

import static java.util.Objects.requireNonNull;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Arrays;
import java.util.List;

import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;

import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;

import indigo.core.ResourceLoaderClassScanner;
import indigo.mybatis.dao.SqlDao;
import indigo.mybatis.dao.SqlDaoFactory;
import indigo.mybatis.entity.EntityMetadata;
import indigo.mybatis.entity.SimpleSqlEntityManager;
import indigo.mybatis.entity.SqlEntityManager;
import indigo.mybatis.entity.annotation.ResultEntity;
import indigo.mybatis.template.TemplateMapperBuilder;
import indigo.mybatis.template.VelocityTemplateMapperBuilder;

@org.springframework.context.annotation.Configuration
public class SqlEntityConfiguration implements ImportAware, ResourceLoaderAware {
	private static final Logger logger = LoggerFactory.getLogger(SqlEntityConfiguration.class);

	private static final TypeFilter[] RESULT_ENTITY_TYPE_FILTERS = {
			new AnnotationTypeFilter(ResultEntity.class, false)
	};

	public static final String DEFAULT_TEMPLATE_LOCATION = "classpath*:META-INF/mapper-template-*.vm";
	public static final String DEFAULT_SQL_SESSION_FACTORY_BEAN_NAME = "sqlSessionFactory";

	private ResourceLoader resourceLoader;

	private String[] basePackages;
	private String[] templateLocations;
	private String sqlSessionFactoryRef;

	@Override
	public void setImportMetadata(AnnotationMetadata importMetadata) {
		AnnotationAttributes attrs = AnnotationAttributes.fromMap(
				importMetadata.getAnnotationAttributes(EnableSqlEntity.class.getName()));
		basePackages = attrs.getStringArray("basePackages");
		templateLocations = attrs.getStringArray("templateLocations");
		sqlSessionFactoryRef = attrs.getString("sqlSessionFactoryRef");
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	private Resource[] resolveTemplateLocations() {
		ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
		List<Resource> resources = Lists.newArrayList();
		for (String templateLocation : templateLocations) {
			try {
				resources.addAll(Arrays.asList(resolver.getResources(templateLocation)));
			} catch (IOException e) {
				throw new IllegalStateException("Failed to resolve templateLocation: " + templateLocation, e);
			}
		}
		return resources.toArray(new Resource[0]);
	}

	@EventListener(ContextRefreshedEvent.class)
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public void onContextRefreshed(ContextRefreshedEvent event) {
		ApplicationContext context = event.getApplicationContext();
		SqlSessionFactory sqlSessionFactory = context.getBean(sqlSessionFactoryRef, SqlSessionFactory.class);
		SqlEntityManager sqlEntityManager = context.getBean("sqlEntityManager", SqlEntityManager.class);

		ResourceLoaderClassScanner scanner = new ResourceLoaderClassScanner(resourceLoader);
		ClassLoader classLoader = resourceLoader.getClassLoader();
		for (String basePackage : basePackages) {
			try {
				for (String className : scanner.getClassNamesRecursive(basePackage, RESULT_ENTITY_TYPE_FILTERS)) {
					try {
						sqlEntityManager.addEntity(classLoader.loadClass(className));
					} catch (ClassNotFoundException e) {
						throw new IllegalStateException("Entity class not found: " + className, e);
					}
					if (logger.isInfoEnabled()) {
						logger.info("Entity found: {}", className);
					}
				}
			} catch (IOException e) {
				throw new IllegalStateException("Failed to scan package: " + basePackage, e);
			}
		}
		Configuration configuration = sqlSessionFactory.getConfiguration();
		for (Resource template : resolveTemplateLocations()) {
			try (Reader reader = new InputStreamReader(template.getInputStream())) {
				String templateName = template.getFilename();
				String templateText = CharStreams.toString(reader);
				TemplateMapperBuilder builder = new VelocityTemplateMapperBuilder(templateName, templateText);
				for (Class<?> entityType : sqlEntityManager.getEntityTypes()) {
					builder.build(configuration, sqlEntityManager.getEntityMetadata(entityType));
				}
			} catch (IOException e) {
				throw new IllegalStateException("Failed to load template: " + template, e);
			}
		}
	}

	@Bean
	public SqlSessionTemplate sqlDaoSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}

	@Bean
	public SqlEntityManager sqlEntityManager() {
		return new SimpleSqlEntityManager();
	}

	@Bean
	public SqlDaoFactory sqlDaoFactory(
			@Qualifier("sqlDaoSqlSessionTemplate") SqlSessionTemplate sqlSessionTemplate,
			SqlEntityManager sqlEntityManager) throws IOException {
		return new SpringSqlDaoFactory(sqlSessionTemplate, sqlEntityManager);
	}

	private static class SpringSqlDaoFactory implements SqlDaoFactory {
		private final SqlSessionTemplate sqlSessionTemplate;
		private final SqlEntityManager sqlEntityManager;

		public SpringSqlDaoFactory(SqlSessionTemplate sqlSessionTemplate, SqlEntityManager sqlEntityManager) {
			this.sqlSessionTemplate = requireNonNull(sqlSessionTemplate, "sqlSessionTemplate cannot be null");
			this.sqlEntityManager = requireNonNull(sqlEntityManager, "sqlEntityManager cannot be null");
		}

		@Override
		public <T> SqlDao<T> getSqlDao(Class<T> entityType) {
			EntityMetadata<T> metadata = sqlEntityManager.getEntityMetadata(entityType);
			if (metadata == null) {
				throw new IllegalStateException("Not an entity type: " + entityType.getName());
			}
			return new SqlDao<>(sqlSessionTemplate, metadata);
		}
	}
}
